<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Json
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Server.php 18951 2009-11-12 16:26:19Z alexander $
 */

/**
 * @see Zend_Server_Abstract
 */
require_once 'Zend/Server/Abstract.php';

/**
 * @category   Zend
 * @package    Zend_Json
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Json_Server extends Zend_Server_Abstract
{
	/**#@+
	 * Version Constants
	 * @const string
	 */
	const VERSION_1 = '1.0';
	const VERSION_2 = '2.0';
	/**#@-*/

	/**
	 * Flag: whether or not to auto-emit the response
	 * @var bool
	 */
	protected $_autoEmitResponse = true;

	/**
	 * @var bool Flag; allow overwriting existing methods when creating server definition
	 */
	protected $_overwriteExistingMethods = true;

	/**
	 * Request object
	 * @var Zend_Json_Server_Request
	 */
	protected $_request;

	/**
	 * Response object
	 * @var Zend_Json_Server_Response
	 */
	protected $_response;

	/**
	 * SMD object
	 * @var Zend_Json_Server_Smd
	 */
	protected $_serviceMap;

	/**
	 * SMD class accessors
	 * @var array
	 */
	protected $_smdMethods;

	/**
	 * @var Zend_Server_Description
	 */
	protected $_table;

	/**
	 * Attach a function or callback to the server
	 *
	 * @param  string|array $function Valid PHP callback
	 * @param  string $namespace  Ignored
	 * @return Zend_Json_Server
	 */
	public function addFunction($function, $namespace = '')
	{
		if (!is_string($function) && (!is_array($function) || (2 > count($function)))) {
			require_once 'Zend/Json/Server/Exception.php';
			throw new Zend_Json_Server_Exception('Unable to attach function; invalid');
		}

		if (!is_callable($function)) {
			require_once 'Zend/Json/Server/Exception.php';
			throw new Zend_Json_Server_Exception('Unable to attach function; does not exist');
		}

		$argv = null;
		if (2 < func_num_args()) {
			$argv = func_get_args();
			$argv = array_slice($argv, 2);
		}

		require_once 'Zend/Server/Reflection.php';
		if (is_string($function)) {
			$method = Zend_Server_Reflection::reflectFunction($function, $argv, $namespace);
		} else {
			$class  = array_shift($function);
			$action = array_shift($function);
			$reflection = Zend_Server_Reflection::reflectClass($class, $argv, $namespace);
			$methods = $reflection->getMethods();
			$found   = false;
			foreach ($methods as $method) {
				if ($action == $method->getName()) {
					$found = true;
					break;
				}
			}
			if (!$found) {
				$this->fault('Method not found', -32601);
				return $this;
			}
		}

		$definition = $this->_buildSignature($method);
		$this->_addMethodServiceMap($definition);

		return $this;
	}

	/**
	 * Register a class with the server
	 *
	 * @param  string $class
	 * @param  string $namespace Ignored
	 * @param  mixed $argv Ignored
	 * @return Zend_Json_Server
	 */
	public function setClass($class, $namespace = '', $argv = null)
	{
		$argv = null;
		if (3 < func_num_args()) {
			$argv = func_get_args();
			$argv = array_slice($argv, 3);
		}

		require_once 'Zend/Server/Reflection.php';
		$reflection = Zend_Server_Reflection::reflectClass($class, $argv, $namespace);

		foreach ($reflection->getMethods() as $method) {
			$definition = $this->_buildSignature($method, $class);
			$this->_addMethodServiceMap($definition);
		}
		return $this;
	}

	/**
	 * Indicate fault response
	 *
	 * @param  string $fault
	 * @param  int $code
	 * @return false
	 */
	public function fault($fault = null, $code = 404, $data = null)
	{
		require_once 'Zend/Json/Server/Error.php';
		$error = new Zend_Json_Server_Error($fault, $code, $data);
		$this->getResponse()->setError($error);
		return $error;
	}

	/**
	 * Handle request
	 *
	 * @param  Zend_Json_Server_Request $request
	 * @return null|Zend_Json_Server_Response
	 */
	public function handle($request = false)
	{
		if ((false !== $request) && (!$request instanceof Zend_Json_Server_Request)) {
			require_once 'Zend/Json/Server/Exception.php';
			throw new Zend_Json_Server_Exception('Invalid request type provided; cannot handle');
		} elseif ($request) {
			$this->setRequest($request);
		}

		// Handle request
		$this->_handle();

		// Get response
		$response = $this->_getReadyResponse();

		// Emit response?
		if ($this->autoEmitResponse()) {
			echo $response;
			return;
		}

		// or return it?
		return $response;
	}

	/**
	 * Load function definitions
	 *
	 * @param  array|Zend_Server_Definition $definition
	 * @return void
	 */
	public function loadFunctions($definition)
	{
		if (!is_array($definition) && (!$definition instanceof Zend_Server_Definition)) {
			require_once 'Zend/Json/Server/Exception.php';
			throw new Zend_Json_Server_Exception('Invalid definition provided to loadFunctions()');
		}

		foreach ($definition as $key => $method) {
			$this->_table->addMethod($method, $key);
			$this->_addMethodServiceMap($method);
		}
	}

	public function setPersistence($mode)
	{
	}

	/**
	 * Set request object
	 *
	 * @param  Zend_Json_Server_Request $request
	 * @return Zend_Json_Server
	 */
	public function setRequest(Zend_Json_Server_Request $request)
	{
		$this->_request = $request;
		return $this;
	}

	/**
	 * Get JSON-RPC request object
	 *
	 * @return Zend_Json_Server_Request
	 */
	public function getRequest()
	{
		if (null === ($request = $this->_request)) {
			require_once 'Zend/Json/Server/Request/Http.php';
			$this->setRequest(new Zend_Json_Server_Request_Http());
		}
		return $this->_request;
	}

	/**
	 * Set response object
	 *
	 * @param  Zend_Json_Server_Response $response
	 * @return Zend_Json_Server
	 */
	public function setResponse(Zend_Json_Server_Response $response)
	{
		$this->_response = $response;
		return $this;
	}

	/**
	 * Get response object
	 *
	 * @return Zend_Json_Server_Response
	 */
	public function getResponse()
	{
		if (null === ($response = $this->_response)) {
			require_once 'Zend/Json/Server/Response/Http.php';
			$this->setResponse(new Zend_Json_Server_Response_Http());
		}
		return $this->_response;
	}

	/**
	 * Set flag indicating whether or not to auto-emit response
	 *
	 * @param  bool $flag
	 * @return Zend_Json_Server
	 */
	public function setAutoEmitResponse($flag)
	{
		$this->_autoEmitResponse = (bool) $flag;
		return $this;
	}

	/**
	 * Will we auto-emit the response?
	 *
	 * @return bool
	 */
	public function autoEmitResponse()
	{
		return $this->_autoEmitResponse;
	}

	// overloading for SMD metadata
	/**
	* Overload to accessors of SMD object
	*
	* @param  string $method
	* @param  array $args
	* @return mixed
	*/
	public function __call($method, $args)
	{
		if (preg_match('/^(set|get)/', $method, $matches)) {
			if (in_array($method, $this->_getSmdMethods())) {
				if ('set' == $matches[1]) {
					$value = array_shift($args);
					$this->getServiceMap()->$method($value);
					return $this;
				} else {
					return $this->getServiceMap()->$method();
				}
			}
		}
		return null;
	}

	/**
	 * Retrieve SMD object
	 *
	 * @return Zend_Json_Server_Smd
	 */
	public function getServiceMap()
	{
		if (null === $this->_serviceMap) {
			require_once 'Zend/Json/Server/Smd.php';
			$this->_serviceMap = new Zend_Json_Server_Smd();
		}
		return $this->_serviceMap;
	}

	/**
	 * Add service method to service map
	 *
	 * @param  Zend_Server_Reflection_Function $method
	 * @return void
	 */
	protected function _addMethodServiceMap(Zend_Server_Method_Definition $method)
	{
		$serviceInfo = array(
            'name'   => $method->getName(),
            'return' => $this->_getReturnType($method),
		);
		$params = $this->_getParams($method);
		$serviceInfo['params'] = $params;
		$serviceMap = $this->getServiceMap();
		if (false !== $serviceMap->getService($serviceInfo['name'])) {
			$serviceMap->removeService($serviceInfo['name']);
		}
		$serviceMap->addService($serviceInfo);
	}

	/**
	 * Translate PHP type to JSON type
	 *
	 * @param  string $type
	 * @return string
	 */
	protected function _fixType($type)
	{
		return $type;
	}

	/**
	 * Get default params from signature
	 *
	 * @param  array $args
	 * @param  array $params
	 * @return array
	 */
	protected function _getDefaultParams(array $args, array $params)
	{
		$defaultParams = array_slice($params, count($args));
		foreach ($defaultParams as $param) {
			$value = null;
			if (array_key_exists('default', $param)) {
				$value = $param['default'];
			}
			array_push($args, $value);
		}
		return $args;
	}

	/**
	 * Get method param type
	 *
	 * @param  Zend_Server_Reflection_Function_Abstract $method
	 * @return string|array
	 */
	protected function _getParams(Zend_Server_Method_Definition $method)
	{
		$params = array();
		foreach ($method->getPrototypes() as $prototype) {
			foreach ($prototype->getParameterObjects() as $key => $parameter) {
				if (!isset($params[$key])) {
					$params[$key] = array(
                        'type'     => $parameter->getType(),
                        'name'     => $parameter->getName(),
                        'optional' => $parameter->isOptional(),
					);
					if (null !== ($default = $parameter->getDefaultValue())) {
						$params[$key]['default'] = $default;
					}
					$description = $parameter->getDescription();
					if (!empty($description)) {
						$params[$key]['description'] = $description;
					}
					continue;
				}
				$newType = $parameter->getType();
				if (!is_array($params[$key]['type'])) {
					if ($params[$key]['type'] == $newType) {
						continue;
					}
					$params[$key]['type'] = (array) $params[$key]['type'];
				} elseif (in_array($newType, $params[$key]['type'])) {
					continue;
				}
				array_push($params[$key]['type'], $parameter->getType());
			}
		}
		return $params;
	}

	/**
	 * Set response state
	 *
	 * @return Zend_Json_Server_Response
	 */
	protected function _getReadyResponse()
	{
		$request  = $this->getRequest();
		$response = $this->getResponse();

		$response->setServiceMap($this->getServiceMap());
		if (null !== ($id = $request->getId())) {
			$response->setId($id);
		}
		if (null !== ($version = $request->getVersion())) {
			$response->setVersion($version);
		}

		return $response;
	}

	/**
	 * Get method return type
	 *
	 * @param  Zend_Server_Reflection_Function_Abstract $method
	 * @return string|array
	 */
	protected function _getReturnType(Zend_Server_Method_Definition $method)
	{
		$return = array();
		foreach ($method->getPrototypes() as $prototype) {
			$return[] = $prototype->getReturnType();
		}
		if (1 == count($return)) {
			return $return[0];
		}
		return $return;
	}

	/**
	 * Retrieve list of allowed SMD methods for proxying
	 *
	 * @return array
	 */
	protected function _getSmdMethods()
	{
		if (null === $this->_smdMethods) {
			$this->_smdMethods = array();
			require_once 'Zend/Json/Server/Smd.php';
			$methods = get_class_methods('Zend_Json_Server_Smd');
			foreach ($methods as $key => $method) {
				if (!preg_match('/^(set|get)/', $method)) {
					continue;
				}
				if (strstr($method, 'Service')) {
					continue;
				}
				$this->_smdMethods[] = $method;
			}
		}
		return $this->_smdMethods;
	}

	/**
	 * Internal method for handling request
	 *
	 * @return void
	 */
	protected function _handle()
	{
		$request = $this->getRequest();

		if (!$request->isMethodError() && (null === $request->getMethod())) {
			return $this->fault('Invalid Request', -32600);
		}

		if ($request->isMethodError()) {
			return $this->fault('Invalid Request', -32600);
		}

		$method = $request->getMethod();
		if (!$this->_table->hasMethod($method)) {
			return $this->fault('Method not found', -32601);
		}

		$params        = $request->getParams();
		$invocable     = $this->_table->getMethod($method);
		$serviceMap    = $this->getServiceMap();
		$service       = $serviceMap->getService($method);
		$serviceParams = $service->getParams();

		if (count($params) < count($serviceParams)) {
			$params = $this->_getDefaultParams($params, $serviceParams);
		}

		try {
			$result = $this->_dispatch($invocable, $params);
		} catch (Exception $e) {
			return $this->fault($e->getMessage(), $e->getCode());
		}

		$this->getResponse()->setResult($result);
	}
}
